package top.hmtools.wxmp.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.SSLContext;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.bouncycastle.crypto.RuntimeCryptoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import top.hmtools.wxmp.core.annotation.WxmpApi;
import top.hmtools.wxmp.core.configuration.WxmpConfiguration;
import top.hmtools.wxmp.core.enums.HttpMethods;
import top.hmtools.wxmp.core.enums.HttpParamType;
import top.hmtools.wxmp.core.exception.HttpRequestFailException;
import top.hmtools.wxmp.core.tools.Bean2MapTools;

/**
 * 基于httpclient以http协议与微信服务器进行通讯，缺省认为微信服务器返回的数据是json字符串，且不能更改。
 * @author HyboWork
 *
 */
public class DefaultWxmpSession implements WxmpSession,InvocationHandler {
	
	private final Logger logger = LoggerFactory.getLogger(DefaultWxmpSession.class);
	
	/**
	 * HTTPclient连接池管理
	 */
	private PoolingHttpClientConnectionManager phccm;
	
	/**
	 * 从连接池中获取的httpclient
	 */
	private CloseableHttpClient hcPool;
	
	private WxmpConfiguration wxmpConfiguration;
	
	public DefaultWxmpSession(WxmpConfiguration wxmpConfiguration) {
		this.wxmpConfiguration = wxmpConfiguration;
		this.initPoolingHttpClientConnectionManager(wxmpConfiguration.getHttpMaxTotal(), wxmpConfiguration.getHttpDefaultMaxPerRoute());
		this.initPoolHttpClient();
	}
	
	/**
	 * 初始化HTTPclient连接池管理
	 * @param maxTotal
	 * @param defaultMaxPerRoute
	 */
	private void initPoolingHttpClientConnectionManager(int maxTotal, int defaultMaxPerRoute){
		if(this.phccm == null){
			synchronized (this) {
				if(this.phccm == null){
					if(maxTotal<1){
						maxTotal = 20;
					}
					if(defaultMaxPerRoute<1){
						defaultMaxPerRoute = 10;
					}
					
					// 使支持 HTTPS
					LayeredConnectionSocketFactory sslsf = null;
					try {
						sslsf = new SSLConnectionSocketFactory(SSLContext.getDefault());
					} catch (NoSuchAlgorithmException e) {
						logger.error(e.getMessage(), e);
					}
					
					//初始化 http池
					Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
							.register("https", sslsf)
							.register("http", new PlainConnectionSocketFactory())
							.build();
					phccm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
					phccm.setMaxTotal(maxTotal);
					phccm.setDefaultMaxPerRoute(defaultMaxPerRoute);
					
				}
			}
		}
	}
	
	/**
	 * 获取连接池特性的httpclient
	 * @return
	 */
	private void initPoolHttpClient() {
		if (hcPool == null) {
			synchronized (this) {
				if(hcPool == null){
					hcPool = HttpClients.custom().setConnectionManager(phccm).build();
				}
			}
		}
	}

	@Override
	public JSONObject httpPostInJsonBody(String url, String jsonString) {
		// 验证入参
		if(StringUtils.isAnyBlank(url,jsonString)){
			throw new IllegalArgumentException("入参为空");
		}
		if(!(url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"))){
			throw new IllegalArgumentException("请求的URL地址必须以“http(s)”开头");
		}
		
		// 组装post请求
		HttpPost httpPost = new HttpPost(url);
		
		// 组装请求参数
		try {
			httpPost.setEntity(new ByteArrayEntity(jsonString.getBytes(this.wxmpConfiguration.getCharset()), ContentType.APPLICATION_JSON));
		} catch (UnsupportedEncodingException e) {
			this.logger.error("http post请求异常",e);
			throw new RuntimeException(e);
		}
		
		//发送请求
		try {
			InputStream contentIS = this.doExecute(httpPost);
			String contentJsonStr = IOUtils.toString(contentIS, this.wxmpConfiguration.getCharset());
			
			if (logger.isDebugEnabled()) {
				logger.debug("获取到微信反馈消息内容原文是：{}", contentJsonStr);
			}
			
			// 解析
			return  JSON.parseObject(contentJsonStr);
		} catch (IOException e) {
			logger.error("http post 请求异常：",e);
			throw new RuntimeException(e);
		}
	}

	@Override
	public JSONObject httpPostInJsonBody(String url, Object obj) {
		return this.httpPostInJsonBody(url, JSON.toJSONString(obj));
	}

	@Override
	public <T> T httpPostInJsonBody(String url, String jsonString, Class<T> clazz) {
		JSONObject jsonObject = this.httpPostInJsonBody(url, jsonString);
		return jsonObject.toJavaObject(clazz);
	}

	@Override
	public <T> T httpPostInJsonBody(String url, Object obj, Class<T> clazz) {
		return this.httpPostInJsonBody(url, JSON.toJSONString(obj), clazz);
	}

	@Override
	public JSONObject httpGet(String url) {
		return this.httpGet(url, JSONObject.class);
	}

	@Override
	public InputStream httpGetAsInputStream(String url, Map<String, String> params) {
		// 验证入参
		if (StringUtils.isBlank(url)) {
			throw new IllegalArgumentException("入参为空");
		}
		if(!(url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"))){
			throw new IllegalArgumentException("请求的URL地址必须以“http(s)”开头");
		}
		
		//补充URL请求参数
		URIBuilder builder;
		try {
			builder = new URIBuilder(url);
			if(params != null && !params.isEmpty()){
				Iterator<Entry<String, String>> iterator = params.entrySet().iterator();
				while(iterator.hasNext()){
					Entry<String, String> item = iterator.next();
					builder.addParameter(item.getKey(), item.getValue().toString());
				}
			}
			String fullUrl = builder.build().toURL().toString();
			
			// 组装get请求
			HttpGet httpGet = new HttpGet(fullUrl);
			
			//执行get请求
			InputStream contentIS = this.doExecute(httpGet);
			return contentIS;
		} catch (URISyntaxException | IOException e) {
			throw new RuntimeException("http get请求异常：",e);
		}
	}

	@Override
	public <T> T httpGet(String url, Class<T> clazz) {
		return this.httpGet(url, null, clazz);
	}

	@Override
	public <T> T httpGet(String url, Map<String, String> params, Class<T> clazz) {
		// 验证入参
		if (StringUtils.isBlank(url) || clazz == null) {
			throw new IllegalArgumentException("入参为空");
		}
		if(!(url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"))){
			throw new IllegalArgumentException("请求的URL地址必须以“http(s)”开头");
		}
		
		try {
			//补充URL请求参数
			URIBuilder builder = new URIBuilder(url);
			if(params != null && !params.isEmpty()){
				Iterator<Entry<String, String>> iterator = params.entrySet().iterator();
				while(iterator.hasNext()){
					Entry<String, String> item = iterator.next();
					builder.addParameter(item.getKey(), item.getValue().toString());
				}
			}
			String fullUrl = builder.build().toURL().toString();
			
			// 组装get请求
			HttpGet httpGet = new HttpGet(fullUrl);
			
			//执行get请求
			InputStream contentIS = this.doExecute(httpGet);
			String contentJsonStr = IOUtils.toString(contentIS, this.wxmpConfiguration.getCharset());
			if (logger.isDebugEnabled()) {
				logger.debug("获取到微信反馈消息内容原文是：{}", contentJsonStr);
			}
			
			// 解析
			return  JSON.parseObject(contentJsonStr,clazz);
		} catch (URISyntaxException | IOException e) {
			throw new RuntimeException("http get请求异常：",e);
		}
	}

	@Override
	public JSONObject httpPostInFormBody(String url, Map<String, Object> params) {
		return this.httpPostInFormBody(url, params, JSONObject.class);
	}

	@Override
	public <T> T httpPostInFormBody(String url, Map<String, Object> params, Class<T> clazz) {
		//验证入参
		if(StringUtils.isBlank(url)){
			throw new IllegalArgumentException("请求的URL地址不能为空");
		}
		if(!(url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"))){
			throw new IllegalArgumentException("请求的URL地址必须以“http(s)”开头");
		}
		if(params == null || params.isEmpty() || clazz ==null){
			throw new IllegalArgumentException("请求参数不能为空");
		}
		
		//组装请求
		HttpPost httpPost = new HttpPost();
		try {
			URIBuilder builder = new URIBuilder(url);
			
			//解析请求参数
			if(params != null && !params.isEmpty()){
				Iterator<Entry<String, Object>> iterator = params.entrySet().iterator();
				while(iterator.hasNext()){
					Entry<String, Object> item = iterator.next();
					String key = item.getKey();
					Object value = item.getValue();
					
					if(value == null){
						continue;
					}
					
					if(value instanceof File){
						//当value为文件类型时
						FileBody fileBody = new FileBody((File) value);
						HttpEntity reqEntity = MultipartEntityBuilder.create()
								.addPart(key, fileBody)
								.build();
						
						httpPost.setEntity(reqEntity);
					}
					else if(value instanceof InputStream){
						//当value为输入流类型时
						byte[] byteArray = IOUtils.toByteArray((InputStream) value);
						// 组装请求参数
						ByteArrayBody byteArrayBody = new ByteArrayBody(byteArray, String.valueOf(System.currentTimeMillis()));
						HttpEntity reqEntity = MultipartEntityBuilder.create().addPart(key, byteArrayBody).build();

						httpPost.setEntity(reqEntity);
					}else{
						//其它类型数据作为 URL 参数
						builder.addParameter(key,value.toString());
					}
					
				}
				
			}
			
			//完整的请求URL
			URI fullUri = builder.build();
			httpPost.setURI(fullUri);
			
			//执行请求
			InputStream contentIS = this.doExecute(httpPost);
			String contentJsonStr = IOUtils.toString(contentIS, this.wxmpConfiguration.getCharset());
			
			if (logger.isDebugEnabled()) {
				logger.debug("获取到微信反馈消息内容原文是：{}", contentJsonStr);
			}
			
			// 解析
			return  JSON.parseObject(contentJsonStr,clazz);
		} catch (URISyntaxException | IOException e) {
			throw new RuntimeException("http post form stream body 请求（文件上传）异常：",e);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getMapper(Class<T> type) {
		// 动态代理
		return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, this);
	}

	@Override
	public void close() throws IOException {
		// TODO 暂不实现
		
	}
	
	/**
	 * 将通过http请求获取的响应数据流转换为指定数据类型的结果对象实例
	 * <br>如果
	 * @param inputStream
	 * @param clazz
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private <T>T generateResultObject(InputStream inputStream,Class<T> clazz){
		if(inputStream == null || clazz == null){
			throw new IllegalArgumentException("入参为空");
		}
		
		//当要求返回字符串类型时，比如 HTML
		if(String.class.equals(clazz)){
			try {
				return (T)IOUtils.toString(inputStream, this.wxmpConfiguration.getCharset());
			} catch (IOException e) {
				throw new RuntimeException("将http响应生成字符串异常：",e);
			}
		}		
		else if(InputStream.class.equals(clazz)){
			//返回原始输入流（有点鸡肋）
			return (T) inputStream;
		}else{
			//通过json字符串转换为对象实例
			String contentJsonStr = null;
			try {
				contentJsonStr = IOUtils.toString(inputStream, this.wxmpConfiguration.getCharset());

				if (logger.isDebugEnabled()) {
					logger.debug("获取到微信反馈消息内容原文是：{}", contentJsonStr);
				}
			} catch (IOException e) {
				throw new RuntimeException("将http响应生成字符串异常：",e);
			}
			
			// 解析
			return  JSON.parseObject(contentJsonStr,clazz);
		}
	}

	/**
	 * 执行HTTP请求，并获取成功的响应数据流
	 * @param request
	 * @return
	 * @throws ClientProtocolException
	 * @throws IOException
	 */
	private InputStream doExecute(HttpUriRequest request) throws ClientProtocolException, IOException{
		// 执行请求
		if(logger.isDebugEnabled()){
			logger.debug("发送的请求原文是：{}",request);
		}
		CloseableHttpResponse httpResponse = this.hcPool.execute(request);

		// 获取HTTP请求响应码
		StatusLine statusLine = httpResponse.getStatusLine();
		int statusCode = statusLine.getStatusCode();
		if (statusCode >= 400) {
			throw new HttpRequestFailException("请求微信服务器反馈异常：" + statusCode, statusCode);
		}
		
		if(statusCode>=300 && statusCode < 400){
			//TODO 跳转指令的处理
			this.logger.info("当前返回http状态码是：{}，对应逻辑暂未实现",statusCode);
		}

		// 获取响应body内容
		return httpResponse.getEntity().getContent();
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		//获取当前被代理方法的注解
		WxmpApi wxmpApi = method.getAnnotation(WxmpApi.class);
		
		if(wxmpApi==null){
			throw new RuntimeException("当前被代理类要执行的方法没有被 top.hmtools.wxmp.core.annotation.WxmpApi 注解修饰");
		}
		
		if(StringUtils.isBlank(wxmpApi.uri())){
			throw new RuntimeCryptoException("uri 不能为空");
		}
		
		//提取方法被调用时的参数（暂时只提取第一个，如果有的话）
		Object params = null;
		if(args != null && args.length>0){
			params = args[0];
		}else{
			params = new HashMap<>();
		}
		
		if(this.logger.isDebugEnabled()){
			this.logger.debug("请求参数原文是：{}",JSON.toJSONString(params));
		}
		
		//获取返回数据类型
		Class<?> returnType = method.getReturnType();
		
		//提取请求URL
		String url = null;
		if(wxmpApi.uri().toLowerCase().startsWith("http://") || wxmpApi.uri().toLowerCase().startsWith("https://")){
			url = wxmpApi.uri()+"?access_token="+this.wxmpConfiguration.getAccessTokenHandle().getAccessTokenString();
		}else{
			url = "https://"+this.wxmpConfiguration.geteUrlServer().toString()+"/"+wxmpApi.uri()+"?access_token="+this.wxmpConfiguration.getAccessTokenHandle().getAccessTokenString();
		}
		
		if(HttpMethods.GET.equals(wxmpApi.httpMethods())){
			//是get请求，参数必须转换成 map，才能方便URIbuild工具将参数补充到 URL中
			String jsonString = JSON.toJSONString(params);
			// FIXME 此处可能有问题，或许应写成：InputStream.class.isAssignableFrom(returnType)
			if(returnType.isAssignableFrom(InputStream.class)){
				//返回类型是 inputStream 的子类的化，则直接返回响应数据流
				return this.httpGetAsInputStream(url, JSON.parseObject(jsonString, HashMap.class));
			}else{
				return this.httpGet(url, JSON.parseObject(jsonString, HashMap.class), returnType);
			}
		}else{
			//是post请求
			if(HttpParamType.FORM_DATA.equals(wxmpApi.httpParamType())){
				//是上传文件，
				Map<String,Object> paramTmp = null;
				if(params instanceof Map){
					paramTmp = (Map<String, Object>) params;
				}else{
					paramTmp = Bean2MapTools.bean2Map(params);
				}
				return this.httpPostInFormBody(url,paramTmp , returnType);
			}else{
				//请求参数数据类型是 json，无论请求参数是Javabean还是map，都方便的使用阿里fastjson转换成json字符串
				return this.httpPostInJsonBody(url, params, returnType);
			}
		}
	}

}
